CrowdStrike API call and retry mechanism#5654
Conversation
Signed-off-by: nsgupta1 <nsgupta1@users.noreply.github.com>
san81
left a comment
There was a problem hiding this comment.
Left a few comments but overall nice progress
| * @param paginationLink An optional pagination URL suffix (used when fetching next pages). | ||
| * @return A {@link CrowdStrikeApiResponse} containing response body and headers. | ||
| */ | ||
| public CrowdStrikeApiResponse getAllContent(Long startTime, Long endTime, String paginationLink) { |
There was a problem hiding this comment.
Please use Instant type for startTime and endTime
There was a problem hiding this comment.
nit: See if using Optional<> for paginationLink adds any redability.
| response.setHeaders(responseEntity.getHeaders()); | ||
| return response; | ||
| } catch (Exception e) { | ||
| log.error("Error fetching CrowdStrike content from URI: {}", uri, e); |
There was a problem hiding this comment.
Use Noisy flag to avoid repeated logging in the case of repeated failures.
log.error(NOISY, "Error fetching CrowdStrike content from URI: {}", uri, e);
There was a problem hiding this comment.
Also, it looks like you are not doing anything here by catching. Do you really want to catch it here?
There was a problem hiding this comment.
I wanted to log the exact URI that's throwing error but I'm also doing it in invokeGetAPI method, I'll remove this try catch block.
|
|
||
| // Convenience method to get a specific header | ||
| public List<String> getHeader(String headerName) { | ||
| return headers.getOrDefault(headerName, Collections.emptyList()); |
There was a problem hiding this comment.
headers itself could be null. Either handle null case or have only one constructor that takes both the arguments and that is the only way to initialize this instance.
There was a problem hiding this comment.
CrowdStrike API returns a header even if API throws an error. I'll create a constructor that takes header and body as inputs.
| //There is still time to renew, or someone else must have already renewed it | ||
| return; | ||
| } | ||
| synchronized (tokenRenewLock) { |
There was a problem hiding this comment.
May be it is good to have this synchronized block inside getAuthToken() method? Otherwise, there is no protection if someone call getAuthToken() concurrently
There was a problem hiding this comment.
@san81 I think we should keep synchronized block inside refreshToken method because getAuthToken is called from initCredentials as well. initCredentials is only called once before we start crawler, it doesn't need synchronization. RefreshToken is the only way to call getAuthToken in multi-thread manner and if multiple threads are trying to refreshToken at the same time we can block them there itself.
| * @throws UnauthorizedException if the API returns 403 (Forbidden) | ||
| * @throws RuntimeException if all retries are exhausted or unexpected errors occur | ||
| */ | ||
| public <T> ResponseEntity<T> invokeGetApi(URI uri, Class<T> responseType) { |
There was a problem hiding this comment.
I would recommend to see the possibility of reusing existing code here. I see the current RestClient code is more inside the Atlassian commons but we can pull that out into the source crawler and make it available for any future saas sources as well.
By reusing, we will minimize the future code maintenance and make sure every plugin gets the future fixes.
I see the additional headers setup in this method which was done here in the Atlassian case. That is also generalizable.
AddressValidation validation is something that you probably don't need as you are not even asking for url from the customer. Apart from that, we can reuse rest of the logic. Lets try that here.
There was a problem hiding this comment.
Thanks @san81 I'll refactor this in the follow up PR.
| sub = URLDecoder.decode(encodedSub, StandardCharsets.UTF_8); | ||
| } catch (IllegalArgumentException e) { | ||
| log.warn("Invalid URL encoding in subfilter: {}", encodedSub); | ||
| continue; |
There was a problem hiding this comment.
is it Ok to continue in this case?
There was a problem hiding this comment.
Yes, it is okay to continue here because there is no point in further validating that sub filter. We will just ignore that subfilter completely and move on to the next one. for example if url is last_updated:>=1745519529+bad%ZZsegment+_marker:<'abc'> we will ignore bad%ZZsegment and santize url last_updated:>=1745519529+_marker:<'abc'>
|
|
||
| @Test | ||
| void testValidEncodedCrowdStrikeUrlPreserved() throws MalformedURLException { | ||
| String url = "https://api.crowdstrike.com//intel/combined/indicators/v1" + |
There was a problem hiding this comment.
nit: double / after crowdstrike.com. probably a typo?
| when(restClient.invokeGetApi(eq(sanitizedUri), eq(CrowdStrikeIndicatorResult.class))) | ||
| .thenReturn(responseEntity); | ||
|
|
||
| CrowdStrikeApiResponse response = service.getAllContent(null, null, paginationLink); |
There was a problem hiding this comment.
is null an acceptable value for startTime and endTime?
There was a problem hiding this comment.
No, startTime and endTime cannot be null anymore because we are passing Instant now and startTime.getEpochSeconds() with throw NPE. I'll add a null check in the method itself. Thanks for catching this.
| /** | ||
| * CrowdStrike service Test | ||
| */ | ||
| class CrowdStrikeServiceTest { |
There was a problem hiding this comment.
See if you can add any test case to validate the searchCallLatencyTimer metric as well.
| * @param paginationLink An optional pagination URL suffix (used when fetching next pages). | ||
| * @return A {@link CrowdStrikeApiResponse} containing response body and headers. | ||
| */ | ||
| public CrowdStrikeApiResponse getAllContent(Long startTime, Long endTime, String paginationLink) { |
There was a problem hiding this comment.
minor: can we rename method and append to code comments to indicate this indicator API if it's not a generic get for all api responses
There was a problem hiding this comment.
Having configurable client timeouts will help
There was a problem hiding this comment.
I'll consider adding this as part of source configuration along with look_back_days in follow up PR.
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
| String filter1 = URLEncoder.encode(LAST_UPDATED + ":>=" + startTime.getEpochSecond(), StandardCharsets.UTF_8); | ||
| String filter2 = URLEncoder.encode(LAST_UPDATED + ":<" + endTime.getEpochSecond(), StandardCharsets.UTF_8); |
There was a problem hiding this comment.
minor: startTimeFilter, endTimeFilter would be better names than filter1, filter2
| @Getter | ||
| @Setter |
There was a problem hiding this comment.
Minor: @Data contains both @Getter and @Setter functionality + has the added bonus of providing a toString implementation for logging
| * Represents the response returned from a CrowdStrike API call. | ||
| */ | ||
| @Getter @Setter | ||
| public class CrowdStrikeApiResponse { |
There was a problem hiding this comment.
nit: can we rename this as CrowdstrikeIntelApiResponse for disambiguation purposes.. since there are other APIs we could be integrating in the future
| return new URI(urlString); | ||
| } else { | ||
| // Manually construct and encode the query string | ||
| String filter1 = URLEncoder.encode(LAST_UPDATED + ":>=" + startTime.getEpochSecond(), StandardCharsets.UTF_8); |
There was a problem hiding this comment.
minor: plz also use CONSTANTS like GREATER_THAN, LESSER_THAN and explain in code comments with an example of what the final url will look like for readability
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Signed-off-by: ngsupta1 <guptaneha.e@gmail.com>
Description
Issues Resolved
Resolves #[Issue number to be closed when this PR is merged]
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.